
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;

using Swaf;
using Swaf.BizRule;

namespace Swaf.FtpServer
{
	/// <summary>
	/// Summary description for FtpFileSysCommandProcessorBizRules.
	/// </summary>
	public class FtpFileSysCommandProcessorBizRules
	{
		public void FtpUSERCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] != null)
			{
				ftpSession.ftpError("500 You are already authenticated\r\n");
				return;
			}
			if(app.sessionData["userId"] != null)
			{
				ftpSession.ftpError("500 username is already specified, please specify password\r\n");
				return;
			}

			string[] param = commandArgs.Split(new char[]{' '});

			// There must be only one parameter - userName
			if(commandArgs.Length > 0 && param.Length == 1)
			{
				string userName = param[0];
							
				ftpSession.sendToClient("331 Password required or user:'" + userName + "'\r\n");
				app.sessionData["userId"] = userName;
			}
			else
			{
				ftpSession.ftpError("500 Syntax error. Syntax:{USER username}\r\n");
			}
		}

		public void FtpPASSCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] != null)
			{
				ftpSession.ftpError("500 You are already authenticated\r\n");
				return;
			}
			if(app.sessionData["userId"] == null)
			{
				ftpSession.ftpError("503 please specify username first\r\n");
				return;
			}

			string[] param = commandArgs.Split(new char[]{' '});

			// There may be only one parameter - password
			if(param.Length == 1)
			{
				string password = param[0];
									
				// Authenticate user
				IResults rc = app.ruleMgr.execute("FtpAuthenticateUser", app.sessionData["userId"], ftpSession, password);
				if(rc.CallResults is bool && (bool)rc.CallResults == true)
				{
					app.sessionData["authenticated"] = true;

					ftpSession.sendToClient("230 Password ok\r\n");
				}
				else
				{						
					ftpSession.ftpError("530 UserName or Password is incorrect\r\n");					
					app.sessionData.Remove("userId"); // Reset userName !!!
				}
			}
			else
			{
				ftpSession.ftpError("-ERR Syntax error. Syntax:{PASS userName}\r\n");
			}
		}

		public void FtpACCTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		/// <summary>
		/// This command allows the user to work with a different
		///	directory or dataset for file storage or retrieval without
		///	altering his login or accounting information.  Transfer
		///	parameters are similarly unchanged.  The argument is a
		/// pathname specifying a directory or other system dependent
		///	file group designator.
		/// </summary>
		public void FtpCWDCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] == null)
				ftpSession.ftpError("530 Please authenticate firtst !\r\n");
			else
			{
				//ftpSession.CurrentDirectory = commandArgs;
				if(true)//m_pServer.OnDirExists(dir))
				{
					ftpSession.sendToClient("250 CDW command successful.\r\n");
				}
				else
				{
					ftpSession.ftpError("550 System can't find directory '" + commandArgs + "'.\r\n");
				}
			}
		}

		public void FtpCDUPCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpSMNTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpQUITCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpREINCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		/*
				 The argument is a HOST-PORT specification for the data port
				to be used in data connection.  There are defaults for both
				the user and server data ports, and under normal
				circumstances this command and its reply are not needed.  If
				this command is used, the argument is the concatenation of a
				32-bit internet host address and a 16-bit TCP port address.
				This address information is broken into 8-bit fields and the
				value of each field is transmitted as a decimal number (in
				character string representation).  The fields are separated
				by commas.  A port command would be:

				PORT h1,h2,h3,h4,p1,p2

				where h1 is the high order 8 bits of the internet host
				address.
			*/
		public void FtpPORTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(isUserAuthenticated(app, ftpSession))
			{

				string[] parts = commandArgs.Split(',');
				if(parts.Length != 6)
				{
					ftpSession.ftpError("550 Invalid arguments.\r\n");
					return;
				}

				string ip   = parts[0] + "." + parts[1] + "." + parts[2] + "." + parts[3];
				int    port = (Convert.ToInt32(parts[4]) << 8) | Convert.ToInt32(parts[5]);

				ftpSession.DataConnectEndPoint = new IPEndPoint(System.Net.Dns.GetHostByAddress(ip).AddressList[0],port);

				//??? ToDo: connect to client, if can't report error there
				ftpSession.sendToClient("200 PORT Command successful.\r\n");
			}
		}

		/// <summary>
		/// This command requests the server-DTP to "listen" on a data
		///	port (which is not its default data port) and to wait for a
		///	connection rather than initiate one upon receipt of a
		///	transfer command.  The response to this command includes the
		///	host and port address this server is listening on.
		/// </summary>
		public void FtpPASVCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] == null)
				ftpSession.ftpError("530 Please authenticate firtst !\r\n");
			else
			{
				TcpListener passiveListener;
				//Find free port
				int port = 6000;
				while(true)
				{				
					try
					{
						passiveListener = new TcpListener(IPAddress.Any, ++port);	
						passiveListener.Start();

						// If we reach here then port is free
						break;
					}
					catch
					{
					}
				}

				// Notify client on what IP and port server is listening client to connect.
				// PORT h1,h2,h3,h4,p1,p2
				string ip = ftpSession.ClientSocket.LocalEndPoint.ToString();
				ip = ip.Substring(0,ip.IndexOf(":"));
				ip = ip.Replace(".",",") + "," + (port >> 8) + "," + (port & 255);

				ftpSession.sendToClient("227 Entering Passive Mode (" + ip + ").\r\n");
				ftpSession.PassiveListener = passiveListener;
			}
		}

		/// <summary>
		///				The argument specifies the representation type as described
		///				in the Section on Data Representation and Storage.
		///				</summary>
		///				<remarks>  Several
		///				types take a second parameter.  The first parameter is
		///				denoted by a single Telnet character, as is the second
		///				Format parameter for ASCII and EBCDIC; the second parameter
		///				for local byte is a decimal integer to indicate Bytesize.
		///				The parameters are separated by a <SP> (Space, ASCII code
		///				32).
		///
		///				The following codes are assigned for type:
		///
		///							\    /
		///				A - ASCII |    | N - Non-print
		///							|-><-| T - Telnet format effectors
		///				E - EBCDIC|    | C - Carriage Control (ASA)
		///							/    \
		///				I - Image
		///	               
		///				L <byte size> - Local byte Byte size
		///				
		///				The default representation type is ASCII Non-print.  If the
		///				Format parameter is changed, and later just the first
		///				argument is changed, Format then returns to the Non-print
		///				default.
		/// </remarks>
		/// <param name="app"></param>
		/// <param name="context"></param>
		/// <param name="commandArgs"></param>
		/// <param name="ftpSession"></param>
		public void FtpTYPECommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] == null)
				ftpSession.ftpError("530 Please authenticate firtst !\r\n");
			else if(commandArgs.ToUpper() == "A" || commandArgs.ToUpper() == "I")
				ftpSession.sendToClient("200 Type is set to " + commandArgs + ".\r\n");
			else
				ftpSession.sendToClient("500 Invalid type " + commandArgs + " .\r\n");
		}

		public void FtpSTRUCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpMODECommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		/// <summary>
		///This command causes the server-DTP to transfer a copy of the
		///file, specified in the pathname, to the server- or user-DTP
		///at the other end of the data connection.  The status and
		///contents of the file at the server site shall be unaffected.
		/// </summary>
		public void FtpRETRCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(isUserAuthenticated(app, ftpSession))
			{
				// ToDo: custom errors 
				FileInfo fi = new FileInfo(ftpSession.calcFullFileName(commandArgs));
				if(!fi.Exists)
					ftpSession.ftpError("550 Access denied or directory dosen't exist !\r\n");
				else
				{
					FileStream fs = null;
					try
					{
						fs = fi.OpenRead();
						if(fs != null)
						{
							// ToDo: bandwidth limiting here

							int readed = 1;
							while(readed > 0)
							{
								byte[] data = new byte[10000];
								readed = fs.Read(data,0,data.Length);
								ftpSession.sendToClient(data,true);
							}
						}

						ftpSession.sendToClient("226 Transfer Complete.\r\n");
					}
					catch(Exception e)
					{
						ftpSession.ftpError("426 Connection closed; transfer aborted.\r\n");
					}
					finally
					{
						if(fs != null)
							fs.Close();
					}
				}
			}
		}

		/// <summary>
		/// This command causes the server-DTP to transfer a copy of the
		///	file, specified in the pathname, to the server- or user-DTP
		///	at the other end of the data connection.  The status and
		///	contents of the file at the server site shall be unaffected.
		/// </summary>
		public void FtpSTORCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(isUserAuthenticated(app, ftpSession))
			{
				// ToDo: custom errors 
				//---- See if path accessible
				FileStream fs = null;
				try
				{	
					Socket socket = ftpSession.getDataConnection();

					FileInfo fi = new FileInfo(ftpSession.calcFullFileName(commandArgs));
					string handlingBizRule = ftpSession.Server.getBizRuleHandler(fi.FullName, true);
					bool processFileNow = true;

					if(handlingBizRule != null)
					{
						IResults rc = app.ruleMgr.execute(handlingBizRule, fi, socket, ftpSession);
						processFileNow = !(rc.CallResults is bool && (bool)rc.CallResults == true);
						if(rc.ExceptionInfo != null)
							throw rc.ExceptionInfo;
					}

					if(processFileNow)
					{
						fs = fi.OpenWrite();

						if(fs != null)
						{
							// ToDo: bandwidth limiting here

							int readed = 1;
							while(readed > 0)
							{
								byte[] data = new byte[10000];
								readed = socket.Receive(data);
								fs.Write(data,0,readed);							
							}
						}
					}

					ftpSession.sendToClient("226 Transfer Complete.\r\n");
				}
				catch
				{ // ToDo: report right errors here. eg. DataConnection read time out, ... .
					ftpSession.sendToClient("426 Connection closed; transfer aborted.\r\n");
				}
				finally
				{
					if(fs != null)
						fs.Close();
				}
			}
		}

		public void FtpSTOUCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpAPPECommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpALLOCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpRESTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpRNFRCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpRNTOCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpABORCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpDELECommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpRMDCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpMKDCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		/// <summary>
		/// This command causes the name of the current working directory to be returned in the reply.
		/// </summary>
		/// <param name="app"></param>
		/// <param name="context"></param>
		/// <param name="commandArgs"></param>
		/// <param name="ftpSession"></param>
		public void FtpPWDCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] == null)
				ftpSession.ftpError("530 Please authenticate first!\r\n");
			else
				ftpSession.sendToClient("257 \"" + ftpSession.CurrentDirectory + "\" is current directory.\r\n");
		}

		public void FtpLISTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			if(app.sessionData["authenticated"] == null)
				ftpSession.ftpError("530 Please authenticate firtst !\r\n");
			else
			{
				DirectoryInfo di = new DirectoryInfo(ftpSession.RealDirectory);
				if(!di.Exists)
					ftpSession.ftpError("550 Access denied or directory dosen't exist !\r\n");
				else
				{
					try
					{
						StringBuilder buff = new StringBuilder();
						foreach(DirectoryInfo dChild in di.GetDirectories())
						{
							buff.Append(dChild.LastWriteTime.ToString("MM-dd-yy hh:mmtt"));
							buff.Append(" <DIR> ");
							buff.Append(dChild.Name);
							buff.Append("\r\n");
						}
						foreach(FileInfo fChild in di.GetFiles())
						{
							buff.Append(fChild.LastWriteTime.ToString("MM-dd-yy hh:mmtt"));
							buff.Append(" ");
							buff.Append(fChild.Length.ToString());
							buff.Append(" ");
							buff.Append(fChild.Name);
							buff.Append("\r\n");
						}
					
						ftpSession.sendToClient(buff.ToString(), true);

						ftpSession.sendToClient("226 Transfer Complete.\r\n");
					}
					catch
					{
						ftpSession.ftpError("426 Connection closed; transfer aborted.\r\n");
					}
				}
			}
		}

		public void FtpNLSTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpSITECommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpSYSTCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpSTATCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public void FtpHELPCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
		}

		public bool processFtpTextFiles(IApplication app, IRuleContext context, FileInfo fi, Socket socket, FtpSession ftpSession)
		{
			bool fileProcessed = false;
			if(socket != null)
			{
			}

			return fileProcessed;
		}

		/// <summary>
		/// This command does not affect any parameters or previously
		/// entered commands. It specifies no action other than that the
		/// server send an OK reply.
		/// </summary>
		public void FtpNOOPCommandProcessor(IApplication app, IRuleContext context, string commandArgs, FtpSession ftpSession)
		{
			ftpSession.sendToClient("200 OK\r\n");
		}

		public bool FtpAuthenticateUser(IApplication app, IRuleContext context, string userid, FtpSession ftpSession, string password)
		{
			return true;
		}

		private bool isUserAuthenticated(IApplication app, FtpSession ftpSession)
		{
			bool areThey = true;
			if(app.sessionData["authenticated"] == null)
			{
				ftpSession.ftpError("530 Please authenticate firtst !\r\n");
				areThey = false;
			}
			return areThey;
		}
	}
}
